const CSV = {
    parse: require('csv-parse'),
    stringify: require('csv-stringify')
};

const YAML = require('yamljs');

const Response = require('./response.js');
const Util = require('./util.js');
const GitPath = require('./gitpath.js');

module.exports = class Gitrows {
    constructor(options) {
        this._defaults();
        this.options(options);
        this._cache = {};
        this.type = options['type'];
        this.cacheTTL = options['cacheTTL'] ? options['cacheTTL'] : 5000; // default is 5econds
    }
    _defaults() {
        const defaults = {
            ns: 'github',
            branch: 'main',
            message: 'GitRowsPack API Post',
            author: {
                name: "GitRowsPack",
                email: "s4nixd@gmail.com"
            },
            csv: {
                delimiter: ","
            },
            strict: false,
            default: null
        };
        this._cache = {};
        Object.keys(this).forEach(key => delete this[key]);
        this.options(defaults);
        return this;
    }
    reset() {
        return this._defaults();
    }
    pull(path) {
        let self = this;
        return new Promise(function(resolve, reject) {
            let headers = {};
            const pathData = GitPath.parse(path) || {};
            if (!pathData.path) reject(Response(400));
            self.options(pathData);
            if (!GitPath.isValid(self.options())) reject(Response(400));
            if (self.user !== undefined && self.token !== undefined && self.ns == 'github')
                headers["Authorization"] = "token " + self.token;
            let url = GitPath.toApi(self.options());
            if (self.ns == 'gitlab') url += "?ref=" + self.branch;
            fetch(url, {
                    headers: headers,
                })
                .then(r => {
                    if (!r.ok) reject(Response(r.status));
                    resolve(r.json())
                })
                .catch((e) => console.error('Error:', e));
        });
    }
    push(path, obj, sha, method = 'PUT') {
        let self = this;
        return new Promise(function(resolve, reject) {
            if (!self.token) reject(Response(401));
            const pathData = GitPath.parse(path) || {};
            if (!pathData.path) reject(Response(400));
            self.options(pathData);
            if (!GitPath.isValid(self.options())) reject(Response(400));
            let data = {
                "branch": self.branch
            };
            if (typeof obj != 'undefined' && obj)
                data.content = Util.btoa(Gitrows._stringify(obj, self.type, self.csv));
            if (typeof sha != 'undefined')
                data.sha = sha;
            let headers = {
                'Content-Type': 'application/json',
            };
            switch (self.ns) {
                case 'gitlab':
                    headers['Authorization'] = "Bearer " + self.token;
                    data.encoding = 'base64';
                    data.commit_message = self.message + Date.now().toString();
                    data.author_name = self.author.name;
                    data.author_email = self.author.email;
                    break;
                default:
                    headers['Authorization'] = "token " + self.token;
                    data.message = self.message + Date.now().toString();
                    data.committer = self.author;
            }
            let url = GitPath.toApi(self.options());

            try {
                if (Util.isEmptyObjectArray(obj) || data.content == 'e30=') {
                    data.content = ""
                }
            } catch (e) {
                console.log('[x] ERROR : ', e)
                reject(Response(500))
            }

            fetch(url, {
                    method: method,
                    headers: headers,
                    body: JSON.stringify(data),
                })
                .then(r => {
                    if (!r.ok) reject(Response(r.status));
                    resolve(Response(r.status));
                })
                .catch((e) => console.error('Error:', e));
        });
    }
    create(path, obj = {}) {
        let method = this.ns == 'gitlab' ? "POST" : "PUT";
        return this.push(path, obj, null, method);
    }
    drop(path) {
        let self = this;
        if (self.ns == 'github')
            return self.pull(path).then(d => self.push(path, null, d.sha, 'DELETE'));
        return self.push(path, null, null, 'DELETE');
    }
    get(path, query, method) {
        let self = this;
        self._meta = {};
        self._data = null;
        return new Promise(function(resolve, reject) {
            const pathData = GitPath.parse(path) || {};
            if (!pathData.path) reject(Response(400));
            self.options(pathData);
            if (!GitPath.isValid(self.options())) reject(Response(400));
            if (pathData.resource) {
                query = query || {};
                query.id = pathData.resource;
            }
            const url = GitPath.toUrl(self.options(), true);
            self._meta.repository = {
                url: url,
                name: pathData.repo,
                owner: pathData.owner,
                ns: pathData.ns || 'github'
            };
            self._meta.file = {
                type: pathData.type,
                mime: Util.mime(pathData.type)
            };
            return self._pullOrFetch(url, method)
                .then(t => {
                    let data = Gitrows._parse(t, self.type);
                    if (data)
                        self._meta.count = {
                            total: data.length || 0
                        };
                    if (data && typeof query != 'undefined') {
                        data = Gitrows._applyFilters(data, query);
                        self._meta.count.query = data.length || 0;
                    }
                    self._data = data;
                    resolve(data);
                })
                .catch(f => console.log(f));
        });
    }
    getDatabases(repo, target = null) {
        let self = this;
        return new Promise(function(resolve, reject) {
            try {
                fetch(`https://api.github.com/repos/${self.owner}/${repo}/git/trees/${self.branch}?recursive=1`)
                    .then(res => res.json())
                    .then(data => {
                        if (target !== null)
                            resolve(data.tree.filter(d => d.path === target).map(d => d.sha)[0])
                        else
                            resolve(data.tree.filter(d => d.type === 'tree').map(d => d.path))
                    });
            } catch (err) {
                console.log(err)
                reject(Response(500))
            }
        });
    }
    getCollections(repo, collection) {
        let self = this;
        return new Promise(async function(resolve, reject) {
            try {
                const tree_sha = await self.getDatabases(repo, collection);
                fetch(`https://api.github.com/repos/${self.owner}/${repo}/git/trees/${tree_sha}`)
                    .then(res => res.json())
                    .then(data => {
                        resolve(data.tree.filter(d => d.type === 'blob').map(d => {
                            return {
                                'collection': d.path,
                                'size': d.size
                            }
                        }))
                    })
            } catch (e) {
                // console.log(e)
                reject(Response(500))
            }
        });
    }
    put(path, data) {
        let self = this,
            base = [];
        return new Promise(function(resolve, reject) {
            self.pull(path)
                .then(
                    d => {
                        base = Gitrows._parse(Util.atob(d.content), self.type);
                        if (self.strict) {
                            self.columns = self.columns || Util.columns(base);
                            data = Util.columnsApply(data, self.columns, self.default);
                        }
                        if (!Array.isArray(base))
                            base = [base];
                        if (Array.isArray(data))
                            base.push(...data);
                        else
                            base.push(data);
                        self.push(path, base, d.sha).then(r => resolve(r)).catch(e => reject(e));
                    }
                )
                .catch(_ => {
                    base = data;
                    self.push(path, base).then(r => resolve(r)).catch(e => reject(e));
                });
        });
    }
    update(path, data, query) {
        let self = this,
            base = [],
            columns;
        const pathData = GitPath.parse(path) || {};
        return new Promise(function(resolve, _) {
            self.pull(path)
                .then(
                    d => {
                        base = Gitrows._parse(Util.atob(d.content), self.type);
                        if (self.strict) {
                            self.columns = self.columns || Util.columns(base);
                            base = Util.columnsApply(base, self.columns, self.default);
                        }
                        if (pathData.resource) {
                            query = query || {};
                            query.id = pathData.resource;
                        }
                        if (typeof query == 'undefined') resolve(Response(304));
                        if (!Array.isArray(base))
                            base = [base];
                        base = Util.valuesApply(base, data, query);
                        self.push(path, base, d.sha).then(_ => resolve(Response(202))).catch(e => e);
                    }
                );
        });
    }
    replace(path, data) {
        let self = this,
            base = [],
            columns;
        const pathData = GitPath.parse(path) || {};
        return new Promise(function(resolve, reject) {
            self.pull(path)
                .then(
                    d => {
                        self.push(path, data, d.sha).then(_ => resolve(Response(202))).catch(e => e);
                    }
                );
        });
    }
    delete(path, query) {
        let self = this,
            base = [];
        return new Promise(function(resolve, _) {
            const pathData = GitPath.parse(path);
            self.options(pathData);
            if (pathData.resource) {
                query = query || {};
                query.id = pathData.resource;
            };
            if (typeof query == 'undefined') resolve(Response(304));
            self.pull(pathData)
                .then(
                    d => {
                        base = Gitrows._parse(Util.atob(d.content), self.type);
                        const diff = Util.where(base, query);
                        const data = base.filter(x => !diff.includes(x));
                        if (JSON.stringify(base) !== JSON.stringify(data))
                            self.push(path, data, d.sha).then(_ => resolve(Response(204))).catch(e => e);
                    }
                );
        });
    }
    columns(path) {
        return this.get(path).then(data => Util.columns(data));
    }
    types(path, query) {
        return this.get(path, query).then(data => Util.types(data));
    }
    options(obj) {
        let self = this;
        const allowed = ['server', 'ns', 'owner', 'repo', 'branch', 'path', 'user', 'token', 'message', 'author', 'csv', 'type', 'columns', 'strict', 'default'];
        if (typeof obj == 'undefined') {
            let data = {};
            allowed.forEach((item, _) => {
                data[item] = this[item];
            });
            return data;
        }
        for (let key in obj) {
            if (allowed.includes(key) && typeof obj[key] !== 'undefined') this[key] = obj[key];
        }
        return self;
    }
    async test(path, constraint = {}) {
        let result = GitPath.parse(path) || {};
        if (!result.valid) {
            if (result.repo && constraint.fragment !== false && constraint.file !== true) result.fragment = true;
            else return {
                ...result,
                ...Response(400)
            };
        } else result.fragment = false;
        const acl = await this._acl(path).then(r => r).catch(e => e);
        result = {
            ...result,
            ...acl
        };
        if (acl.code) {
            result.valid = false;
            result.level = 'repo';
            return {
                ...result,
                ...Response(acl.code)
            };
        } else {
            result.valid = result.fragment;
            result = {
                ...result,
                ...result.permissions
            };
            delete(result.permissions);
        }
        if (!result.fragment) {
            const file = await this._isRepoFile(path).then(r => r).catch(e => e);
            result.valid = file === true;
            result.level = 'file';
            result = {
                ...result,
                ...Response(file === true ? 200 : 404)
            };
        }
        Object.keys(constraint).forEach((item, _) => {
            if (result[item] !== constraint[item]) {
                result.valid = false;
                if (result.message && Array.isArray(result.message.description))
                    result.message.description.push(`Constraint Violation - ${item} must not be ${result[item]}`);
                else
                    result = {
                        ...result,
                        ...Response(400, {
                            description: [`Constraint Violation - ${item} must not be ${result[item]}`]
                        })
                    };
            }
        });
        return result;
    }
    _acl(path) {
        let self = this;
        const test = GitPath.parse(path) || {};
        if (!test.repo)
            return Promise.reject(Response(404));
        if (test.ns != 'github')
            return Promise.reject(Response(501));
        const hash = `acl:${test.ns}:${test.owner}:${test.repo}`;
        if (typeof self._cache[hash] != 'undefined')
            if (self._cache[hash]['at'])
                if ((Date.now() - self._cache[hash]['at']) < self.cacheTTL)
                    return new Promise((resolve, reject) => self._cache[hash]['as'] ? resolve(self._cache[hash]['as']) : reject(Response(404)));
        let headers = {
            'Content-Type': 'application/json',
        };
        if (self.user && self.token)
            headers['Authorization'] = "token " + self.token;
        return fetch("https://api.github.com/repos/" + test.owner + '/' + test.repo, {
            headers: headers
        }).then(r => {
            if (!r.ok) {
                self._cache[hash]['as'] = null;
                return Response(404);
            }
            return r.json().then(data => {
                const acl = {
                    'private': data.private,
                    'permissions': data.permissions
                };
                self._cache[hash]['as'] = acl;
                self._cache[hash]['at'] = Date.now();
                return acl;
            })
        }).then(r => r).catch(e => e);
    }
    _listPushableRepos(ns, owner, grouped) {
        const self = this;
        if (typeof ns == 'undefined')
            return Promise.reject(Response(400));
        ns = ns.replace('@', '');
        if (typeof owner == 'undefined')
            [ns, owner] = ns.split('/');
        if (ns != 'github')
            return Promise.reject(Response(400));
        if (!self.user || !self.token)
            return Promise.reject(Response(403));
        const hash = `repos:${ns}:${owner}:${!!grouped}`;
        if (typeof self._cache[hash] != 'undefined')
            if (self._cache[hash]['at'])
                if ((Date.now() - self._cache[hash]['at']) < self.cacheTTL)
                    return new Promise((resolve, reject) => self._cache[hash]['as'] ? resolve(self._cache[hash]['as']) : reject(Response(404)));
        let headers = {
            'Content-Type': 'application/json',
            'Authorization': "token " + self.token
        };
        return fetch("https://api.github.com/user/repos?per_page=100", {
            headers: headers
        }).then(r => {
            if (!r.ok) {
                self._cache[hash]['as'] = null;
                return Response(404);
            }
            return r.json().then(r => {
                self._cache[`raw:repos:${ns}:${owner}`] = r;
                let repos;
                if (!!grouped) {
                    repos = {};
                    r.map(i => repos[i.owner.login] = {
                        avatar: i.owner.avatar_url,
                        type: i.owner.type,
                        login: i.owner.login
                    });
                    Object.keys(repos).forEach((item, _) => {
                        repos[item]['repos'] = r.filter(f => f.permissions.push && f.owner.login == item).map(r => `@${ns}/${r.full_name}`);
                    });
                    repos = (typeof owner != 'undefined') ? repos[owner] : repos;
                    self._cache[hash]['as'] = repos;
                    self._cache[hash]['at'] = Date.now();
                    return repos;
                }
                repos = r.filter(f => f.permissions.push).map(r => `@${ns}/${r.full_name}`);
                if (typeof owner != 'undefined') repos = repos.filter(r => ~r.indexOf(`@${ns}/${owner}/`));
                self._cache[hash]['as'] = repos;
                self._cache[hash]['at'] = Date.now();
                return repos;
            })
        }).then(r => r).catch(e => e);
    }
    _listRepoContents(ns, owner, repo, branch) {
        let self = this;
        let test = GitPath.parse(ns);
        if (test.repo) {
            ns = test.ns;
            owner = test.owner;
            repo = test.repo;
            branch = test.branch;
        }
        if (!ns || !owner || !repo)
            return Promise.reject(Response(400));
        if (ns != 'github')
            return Promise.reject(Response(501));
        const hash = `files:${ns}:${owner}:${repo}`;
        if (typeof self._cache[hash] != 'undefined')
            if (self._cache[hash]['at'])
                if ((Date.now() - self._cache[hash]['at']) < self.cacheTTL)
                    return new Promise((resolve, reject) => self._cache[hash]['as'] ? resolve(self._cache[hash]['as']) : reject(Response(404)));
        let headers = {
            'Content-Type': 'application/json',
        };
        if (self.user && self.token)
            headers['Authorization'] = "token " + self.token;
        return fetch("https://api.github.com/repos/" + owner + '/' + repo + '/git/trees/' + branch + '?recursive=1', {
            headers: headers
        }).then(r => {
            if (!r.ok) {
                self._cache[hash]['as'] = null;
                return Response(404);
            }
            return r.json().then(data => {
                if (typeof data.tree == 'undefined' || !Array.isArray(data.tree))
                    return Response(404);
                let contents = [];
                data.tree.forEach((item, _) => {
                    contents.push(item.path);
                });
                self._cache[hash]['as'] = contents;
                self._cache[hash]['at'] = Date.now();
                console.log('contents: ', contents)
                return contents;
            })
        }).then(r => r).catch(e => e);
    }
    _isRepoFile(path) {
        let self = this;
        const test = GitPath.parse(path) || {};
        return self._listRepoContents(path).then(c => c.findIndex(item => test.path.toLowerCase() === item.toLowerCase()) > -1).catch(e => e);
    }
    _getRepoTree(ns, owner, repo, branch) {
        let self = this;
        return self._listRepoContents(ns, owner, repo, branch).then(c => {
            let result = {};
            c.forEach(p => p.split('/').reduce((o, k) => o[k] = o[k] || {}, result));
            return result;
        }).catch(e => e);
    }
    static _applyFilters(data, query) {
        data = Util.where(data, query);
        let aggregates = Object.keys(query)
            .filter(key => key.startsWith('$'))
            .reduce((obj, key) => {
                obj[key] = query[key];
                return obj;
            }, {});
        if (Object.keys(aggregates).length)
            data = Util.aggregate(data, aggregates);
        return data;
    }
    static _stringify(obj, type = 'json', options = {}) {
        try {
            switch (type.toLowerCase()) {
                case 'csv':
                    return Array.isArray(obj) ? CSV.stringify(obj, Object.assign({
                        header: true
                    }, options)) : null;
                case 'yaml':
                    return YAML.stringify(obj, 10);
                default:
                    return JSON.stringify(obj, null, 2);
            }
        } catch (e) {
            return null;
        }
    }
    static _parse(content, type = 'json') {
        type = type.toLowerCase();
        try {
            switch (type) {
                case 'csv':
                    return CSV.parse(content, {
                        columns: true,
                        skip_empty_lines: true,
                        cast: true
                    });
                case 'yaml':
                    return YAML.parse(content);
                default:
                    return JSON.parse(content);
            }
        } catch (e) {
            return null;
        }
    }
    _pullOrFetch(url, method = 'fetch') {
        let self = this;
        if (method == 'pull') {
            return self.pull(GitPath.fromUrl(url)).then(p => {
                self._meta.repository.private = true;
                return Util.atob(p.content)
            }).catch(e => e);
        }
        let headers = {
            "cache-Control": "no-cache",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"
        }
        return fetch(url, {
                headers: headers
            })
            .then(
                r => {
                    if (r.ok) {
                        self._meta.repository.private = false;
                        return r.text()
                    };
                    //retry by api if token is present
                    if (self.user !== undefined && self.token !== undefined && self.ns == 'github') {
                        return self.pull(GitPath.fromUrl(url)).then(p => {
                            self._meta.repository.private = true;
                            return Util.atob(p.content)
                        }).catch(e => e);
                    }
                    return Response(r.status);
                }
            );
    }
}
